Welcome back.
We're going to jump right into this lecture by starting to script our client sided drink class.
So let's go ahead and get started.
The services we're going to need in our client side of drink class of course is going to be replicated
storage.
That way we can reference all of the assets for our SCP.
We're going to need the run service as well to verify that whatever script is requiring this module
script is running on the client.
We're going to need the tween service, and we're going to use this to tween some GUI elements for,
you know, the drink messages on the screen.
And of course, we're going to have the player service to refer to the local player when we need to.
Now, some variables we're going to need, of course, is to check whether or not the script is running
on the client.
So I'm calling it is client.
And inside the run service there's a function for is client.
If this got required by the server for some reason then we're just going to return an empty table and
that's it.
Otherwise we can set up the same thing like we did in our server class.
We'll have a table to represent the class and a meta table to hold all the functions.
And we'll make sure to set the underscore underscore index meta method to the drink meta table.
And then we're going to refer to things like the request event, which is in replicated storage.
And since we're on the client and I plan to have this module script get required by the local script
that's in the GUI, and since that's in the starter GUI, we don't know if this is going to be replicated
right away.
So we're going to wait for this SCP 294 assets folder.
And then we're also going to wait for the request remote function in case it hasn't been replicated.
We'll do the same thing for comms event.
Remember, we don't have to wait for that folder again because we did it right here and get the comms
event.
I'm also going to have a table in here called Current Drinks, and this is going to store all of the
drinks that exist on the client.
So any drink that the player has in their inventory, the objects for those drinks are going to be stored
in this table.
So we'll have a separate one here on the client.
And the server has one on its end as well right here.
And then I'm also going to require that module script that stores all the functions for the drinks.
So I'll call it drink functions.
We're going to require and replicated storage dot SCP 294 assets.
We're going to wait for the server side drink class, because a child of that drink class is going to
be the pre-made drink functions.
And that's the module script we're going to require, just in case we need to execute any functionality
for some specific drinks.
And lastly, we're also going to create a new RNG data type.
All right.
So now the only private functions we're going to need in here are actually ones we've already created.
So it can go to our drink class on the server.
And we can just copy these two.
Functions right here and paste those into here, because these are going to be the exact same functions
we're going to need here on the client.
We're going to check arguments.
And we're also going to be able to destroy drink objects on the client as well.
Some other private functions we're going to need are for some events we'll use on the cup tool itself.
So we want to listen for when that cup tool gets equipped, when it gets unequipped and when it gets
activated.
That way, you know, we can play animations and we can consume the drink when the player activates
the tool.
So we'll have one function, we'll call it on equipped.
And this is going to be for when our drink cup gets equipped.
We're going to have a function for when it gets unequipped.
So we'll call it on unequipped pass self to it as well.
We're going to have a function for when the drink gets activated.
So on activated again we'll pass self.
And then one more function I want to have is to fade that message.
Like when we drink the liquid out of our cup, we're going to get that message on the bottom of the
screen that tells us what's going on, gives us context for the drink, so we can call this function
fade DUI message.
Again, we're going to pass self to it.
And in here is where we'll, you know, fade out the text or fade in the text.
Set the text to be, you know, whatever is for the specific object or cup that's being consumed and
stuff like that.
Now, instead of public functions, we're going to create a new constructor.
But this constructor is going to be a little bit different than the one that's going to be on our server
script.
Specifically, we don't have to create a new type in here, because the only thing I want to pass to
this constructor one is going to be the guy that's going to contain the text we need to update on the
screen for our UI message.
So we need to pass a UI to this function, which is a screen UI.
We're also going to need the drink name that the player inputted.
That way, we can create a new object based on the name of the drink we entered.
So if they want a cup of air, they'll pass air here.
And then we can use the remote function to invoke the server to create a new drink object, and then
get that object back here on the client.
And using that new drink object, we can attach all of the client side stuff to it for us.
And then the last thing I'll need is an optional parameter.
I'm going to call it ID.
And this is going to be optional because there might be a time where we need to use this constructor
to either a create a new drink and get that drink from the server, or b create a new drink object on
the client.
But that drink object already exists on the server and we just need to get it on the client.
So let's go ahead and start filling this out.
We're obviously going to check the argument guy to make sure it is of the type screen guy or of the
class screen guy.
We're going to check our drink name just to make sure you know it is indeed a string.
And then we're not going to check the ID because it's an optional parameter.
But what we're going to do here is I'm going to create two variables.
I'm going to call one success and result.
And this is going to store the information that gets returned from our remote function.
And then what I'm going to do now is I'm going to check if we were passed an ID to our constructor.
So if we were not passed an ID aka id is nil, then what we could do is we could set success and result
equal to the request event.
And we're going to invoke the server to do a particular action.
And that action is create and get a drink object.
So remember here in our server script, if we go down to our remote function we have the ability to
create and get a drink object.
So it's going to go in there and create a drink object gives it a player.
And then the key for the drink.
So that means we're going to have to pass the drink name, which is also the key.
Otherwise we can put an else statement here, meaning that we did get passed an ID.
And if this constructor gets passed an ID, then that means we automatically know that there's an object
that already exists on the server with this particular ID.
So instead we could set success and result equal to the request event.
And we're going to invoke server request.
The action is request drink object.
So we're not creating a new drink object.
We're just getting one that already exists.
And remember we need to pass them that ID.
So inside of here in our remote function we can request a drink object right here.
And we pass to them the ID.
And we can check to see on the server if that object exists and whether or not the player is supposed
to be the owner of it.
And if they are, then we'll return the object back to them on the client.
Now, the way I plan to set this up is that every time a cup or a cup tool gets added to the backpack
of a player, we're going to have a function that's listening to whenever it gets added, and then it's
going to request to get the drink object for that specific drink cup based on the ID.
However, we don't want to do that if we've already gotten the object on the client.
So for example, when we create a new drink object on the server and we give it to the player in their
inventory, then that function will fire and get the object on the client, and we'll store it inside
of our current drinks table.
But let's say they equip it and then unequip it.
Well, that means that event's going to get fired again, because that drink cup got re-added back into
our inventory.
And we don't want to create, you know, another drink object.
So the plan is before we set success and result, we need to check whether or not that our drink exists
within our current drinks function.
And the way we could do that is we could copy a function from our drink class on the server, which
is the get object from ID.
So we're going to copy this and put this here in our function.
So this is going to do the exact same thing.
It's going to loop through our current drinks table on the client side.
And it's going to check if we already have a drink object with that ID.
If we do then we don't have to request another drink object.
We already have it.
So we could set result equal to drink.
Get object from id and pass the id that gets passed to our constructor.
And if we already have a result, aka we already have the object on the client, then we're just going
to return and do nothing with this constructor.
Another thing we're going to have to watch out for is in case the invoke server function errors and
success is set to false, meaning some error happened so we could check.
If not success, then we can just warn on the client and we'll create a string unable to create or get
drink object on client.
And then we can say the error is we'll pass a directive here for a string.
And that means we can wrap this string in parentheses and call the format function on it.
And what's going to replace that directive.
Well the result from the invoke server function which should be an error message.
So for example.
Inside of our on server invoke function.
If we try to do something in here, let's say we accidentally pass, you know, the wrong action or
something.
It's going to return false and give us an error message here.
And we're going to take that string and we're going to put it in here using the format function.
So that way it's a little bit easier for us to debug problems on the client.
And of course we don't want to forget to return because the Warn function will not halt the thread of
execution.
Otherwise, if everything checks out good, then we can set the self variable equal to the result from
either this function or this function.
Whichever action returns to us our drink object.
And because of that limitation I talked about in the previous lecture, where the meta table, when
it gets passed through remote functions or remote events, the meta table is lost.
So that means we're going to have to reattach a new meta table to our object, which is going to contain
all the functions on the client.
And that's pretty easy to do.
All we can do is call the set meta table function pass self, and then attach our drink meta table here
on the client.
And now this drink object on the client is going to have access to all the client related functions.
Now another thing I want to do is I want to get a reference to the animator that is inside of the players,
uh, humanoid.
So what we could do is that since this object is automatically going to have the properties from the
server, which is going to be all of these, we can get the owner property as well.
So we could do self dot owner get their character.
And this is referring to the local player or us get our humanoid and then get the animator inside of
our humanoid.
And then using this animator we can load the animations that are in our drink cup.
So let's set up some other properties for our object on the client.
So first I want to have a reference to the guy that's going to contain our drink label.
So we'll just do self dot guy is equal to the guy that gets passed to this function.
We're going to have a debounce in here that keeps track of whether or not we've already drank our cup
on the client, so we'll call it drink Debounce and we'll set it to false as default.
And then from here we can load the animations, which will be our idle track for our drink cup and our
consumption track for our drink cup.
So one will be idle track, and we're going to use the animator to load an animation.
And actually let's use type annotation and denote this as an animator.
So that way we can actually see all the functions.
There we go load animation and we're going to pass self dot cup.
And inside of our cup.
If you remember there's a folder for animations and we have our two animations in there.
So self dot cup get the animations folder.
And this is going to be the idle animation.
So we're loading this animation on our humanoid.
And then we could do the same thing for the consume track or the consume animation.
We'll get a track back from this function.
Self dot cup dot animations dot consume.
The next thing I want to do is I want to create a new table in here.
I'm going to call it connections.
And this table is going to store all the instances that get returned from the connect function.
So, you know, when we listen to events specifically we're going to be listening to events on the tool.
It's going to return an object back to us that is used to, you know, listen to that specific event
and we're able to connect or disconnect that, uh, function to that particular event.
And when the cup gets destroyed, we want to, you know, disconnect those events.
So inside of self dot connections we can have an event for when the cup gets activated.
I'll just create a new key value pair.
I'll call it activated.
And at this key we're going to store self dot cup dot activated.
Activated, we're going to connect a function to it.
And this function is going to call the on activated function.
And we're going to pass the object to it.
And remember, this function is going to return to us.
I believe it's an R script connection or event connection.
It's something like that, some kind of instance.
And we can use that instance to disconnect this lambda function from this particular event when we need
to.
And then we could do the same thing for all of the other events we need to listen to.
We need to listen for when our cup gets equipped.
So at this key value pair, we can just store cup dot equipped and connect a lambda function that calls
the on equip function pass self to it.
And we'll do one for the unequipped event as well.
Past self and put the end statement.
And I think that's all we need to do.
So the last thing we must complete is to insert.
This new drink object on the client into our current drinks table.
So now that we have this connections table here, there's actually a modification that I want to make
to our destroy drink object.
And I want to loop through that table and disconnect all of these connections.
Now that's not going to be an issue because when we destroy our drink object, we're actually destroying
the cup on the client as well as on the server, which automatically destroys all of the connections
that we've attached to the cup and its specific events.
But just in case we have maybe other connections in here for some events that aren't related to the
Cup, we want to disconnect those functions.
So what we can do.
Is we can loop through every single key value pair inside of that table.
So we'll call this connection in pairs self dot connections.
And what we're going to do is we're going to get that connection and just disconnect it.
And in fact, we're going to do the same thing on the server.
But that's going to be in a later lecture because we're going to have to disconnect a specific event.
You might remember what that event is, but we're going to get to that later.
So don't worry about disconnecting events on the server class for now.
Instead, let's go ahead and fill out these functions down here.
These ones should be pretty easy to fill out, because all I want to do when the cup gets equipped is
to play our idle track, so we can refer to self and get the idle track property that we created in
there.
And then we can just call the play function on it.
So now we'll be holding our cup just the way we want to.
And when we unequip our cup then we can just stop the idle track.
And then when our cup gets activated, what we could do is we'll have a function in here for consuming
it.
So we'll have a function to consume the drink.
And we'll go ahead and make that in the drink meta table right now.
So we'll call it consume.
So whenever our cup gets activated, we'll call the consume event and play.
You know, the consume, drinking, animation and all that kind of fun stuff.
Now what do I want to do inside of our fade guy message function?
Well, first off, I want to get a reference to the text label that's going to store the text we want
to display on the screen.
And that's going to be equal to self dot g.
Remember we made the G property.
And inside of our 294 guy there is a text label in here called drink message label.
So we'll do dot drink message label.
And we'll denote this as a text label.
Because the linter isn't able to figure out on its own what exactly drink message label is.
So this function we're going to call when we consume our drink.
And that means we need to set the text in this label and then fade that text onto the screen.
And then after a few seconds, fade it back out again.
However, if you remember, some of our drinks could have a table for the consume messages.
So there could be multiple messages that our cup could display.
So we need to make a check for that.
So we could do if type of self dot consume message is equal to table.
So we know that the consume message are multiple different strings.
Then we can pick a random string to display on the screen.
So what I'll do is I'm actually going to create a variable called a ridge message or original message.
I'm not going to instantiate it yet, but in here I'm going to set original message equal to self dot
consume message.
And we're going to pick out a random message using the RNG next integer function.
And we'll do one to the max number of self dot consume messages.
So we pick a random string and we set that as the original message.
Otherwise we'll just set original message equal to self dot consume message.
And then from this point um, to fade the guy onto our screen, we first have to verify that the guy
is completely invisible.
So let's say the player has multiple drinks in their inventory and they already drink when previously,
and that text label was already faded onto their screen.
Well, that means we'll want to fade it out.
Instead, replace the text and then fade it back in.
So we need to check if the text label transparency is equal to one.
So if it's completely invisible then that's good.
We can do text label dot text equal to original message.
And then we'll use the tween service to create a new tween on our text label.
We can make the tween last, I don't know, just maybe one second long.
And the thing we need to tween is the transparency of our text.
And we need to set that to zero.
And then we can just call the play function on this.
So if our text is invisible, then we're going to set the text to the original message, which could
be something like um, there is nothing in the cup, so we'll set it to there is nothing in the cup.
And then we tween the text to be visible on the screen.
And then afterwards we'll wait some arbitrary amount of time, like six seconds, and then we'll fade
the text back out of the screen.
But we'll need to put an else statement here.
And this else statement is for let's say our drink message label is already visible on the screen,
and a different drink has already set a message.
Well, then we'll, um, we're going to create a tween because we need to refer to a tween event.
So tween service create.
And actually we can just copy the same thing here.
And we'll not call play on it.
But what we need to do is we need to set the text transparency to one.
Because if the text label transparency is anything but one, which means it's visible on the screen,
then we need to tween it to be invisible again before we can change the text.
So we're going to play this tween, and then we're going to wait for this tween to finish before we
change the text.
Once it finishes then we can do text label dot text is equal to original message.
And then we can do this exact same tween here again.
And play it on the screen.
And actually, a better way to do this would be to just copy this, put it here.
And that way you don't have to copy and paste this function in the same spot.
After that, we're going to wait for six seconds, and then after we wait for six seconds, we want
to tween the text label to be invisible again.
However, there is a small problem with this.
Let's say in those six seconds we're waiting here, the player consumes another drink and that drink
fades out the guy and sets the text to something different.
Well, that's why I decided to store the text in a variable called Original Message, because we're
going to compare if the message inside of the text label is still the same.
So if the text label and we're going to check the text transparency first.
So if the text label is still visible.
And the text label dot text is still equal to the original message, meaning that no other drink changed
the message on the screen or faded out the guy.
Then we're in the clear to go ahead and fade out the guy so we can again just copy this function here,
and instead we're going to set the text transparency to one.
So it's very important that we have our Fe UI function set up like this, just in case it doesn't conflict
with any other drinks that may be fading in and out the text label on our screen.
So let's go ahead and go back down and fill out our consume function.
So what do we want to do when we, you know, click the drink and we try to consume it.
Well first off before we do anything remember that drink Debounce property I made.
We need to check if that's true.
So if drink debounce is true, then we're just going to return because we can't drink more than one
side of the same cup.
Otherwise, if it isn't true, then we're going to set it to true.
That way we can't execute this function again more than once.
After that, we're going to have to check if the self dot is empty.
Property is true.
Remember on the server we have a property in there to set whether or not the drink is empty when it
gets given to the player, because if they have a cup of air, it's going to be empty by default and
you know you can't drink it, so there's no point playing a drinking animation.
So instead, if the cup is empty, then what I want to do is I want to call the phage UI message function
and pass self to it.
After we do that, we can, you know, just destroy the cup.
But let's say the cup isn't empty.
Well, then, what I want to do is I want to refer to our consumed track, and I want to play that.
And then I'm also going to wait for when an event in this consumed track gets reached.
So let me go back to my drink animation rig here.
And I'm going to go to the avatar tab.
Open up my animation editor.
When I select my drink animation rig here, you'll see that in my consume animation, I've actually
included an event here.
And this event is called consume.
So I don't want to play the consume sound or do any of the effects for my drink until we hit this event
in my animation.
So how do we listen for when we hit that event?
Well, there's a neat little function called git marker reached signal.
And we can pass the name of the event which is consume.
And we're going to wait for this event to get reached.
Once this event gets reached then we're unclear to go ahead and do self dot consume sound and play our
consume sound.
And then we can also fade out our message and pass self to it.
And then we're also going to fire to the server to consume the drink.
That way, the liquid inside of our cup disappears for everyone in the game.
And if you remember, our consume drink action requires for us to let me look.
It requires for us to pass the ID of the object, which shouldn't be too difficult.
We can just do self.id.
And then one more thing I want to check is whether or not this drink has any functions inside of our,
uh, drink functions module script.
So we can check if drink functions dot current functions.
Self dot premade key.
So if there is a key value pair inside of the current functions for this particular drink, then we're
going to run that function.
So we'll do dot handler and pass the player.
Which would be self dot owner.
And then we'll pass the object itself.
And in fact, this is actually a little redundant.
We can just pass self itself.
So let me do that on the server end as well.
Go back to your server script for the drink class on the server.
And when we call that handler here it is.
Instead we can just pass the object to it because the object will have that owner property inside of
it.
So that means if I go back to my premade drink functions module script, I don't have to define a player
here.
Instead, I can just get my drink object by itself and then refer to the owner property that is inside
of that particular drink object.
Anyways, we'll call that function and it'll go through there and run all the code for that particular
drink.
Um, otherwise, if we do not have a function for that particular drink, then what I'll do is I'll
wait for the consume track to, uh, finish animating.
So there's an event called stop, and we're going to wait for that to fire, and we're going to wait
for our animation to end before we destroy our drink cup.
However, another issue you might realize is that our fade guy message function actually yields.
Remember, we have a task dot wait six seconds in here.
So that means when we're calling this function here, the thread of execution hops into this function
and executes stuff in there, and it won't be able to move forward until that.
Wait.
Uh, six seconds is up.
So we're actually yielding here, and we're yielding here.
So we won't be able to actually execute the functionality for the drink or fire to the server to, you
know, consume the drink until those six seconds are up.
So how do we fix that?
Well, we can just use the task dot spawn function to spawn this function in a new thread.
So.
We just pass a reference to our phage UI message, and then to be able to pass the object itself to
our phage UI message function, we just pass it as a second parameter to the task spawn function.
And then we could do the exact same thing right here.
So the tasked spawn function is going to execute this function in a separate thread and it'll pass self
to automatically.
Alrighty then.
I guess the next thing we'll have to do is define our destroy function here on the client.
So let's go ahead and do that.
Inside of our drink meta table we'll create a new function called destroy.
And what do we want to do when this function gets called?
Well, because we're going to be specifically calling this function on the client.
That means we also want to destroy the drink object that exists on the server.
So I'm going to create two variables, one called success and the other called result because we're
going to call the invoke server function for our request remote function.
So invoke server.
The action we want the server to do is to destroy a drink object.
And when we use that action, we need to pass the ID for that object.
So we're going to pass self.id.
And then we're going to check if we were successful in destroying that drink object on the server.
If we were then we can call the destroy drink object on the client and destroy it in this module script
as well.
Otherwise, if we weren't successful in destroying it, then we can just print something out on the
client and say warn unable to destroy drink object and we can give the error message using a directive.
So again percentage S, we can wrap this string inside of parentheses and call the format function on
it.
And what do we want to replace at that directive.
Well the same thing we did before which is going to be the result or AKA whatever string gets returned
from our on server invoke function.
All right.
This is looking pretty good so far.
Uh, I think the next section we can fill out is for the communication event.
So comms event, we're going to access the on client event, uh, script signal or this event.
And we're going to connect a function to it.
And if you remember inside of our drink class, when we use the fire client function, we're firing
to a specific player and we're giving them an action and then some arguments along with that action.
So for example if the destroy function gets called on the server.
Then we want to tell the client to destroy a particular drink object.
So we can again define the action the server wants us to do, and then the arguments supplied to that
action.
And since we only have one action we're using so far, we can do if action is equal to destroy drink
object.
Then we can check which object they want us to destroy by getting it from the drink.
Get object from id and pass args one, which is the ID of the object the server wants to destroy.
If we have that object in our inventory, then what I want to do is I don't want to call object destroy.
Why don't we want to do that?
Well, because there's no point if the server is already telling us to destroy a drink object.
The server already destroyed it on their end.
And remember, in our destroy function, we're requesting for the server to destroy a drink object.
But that object isn't going to exist if the server already deleted it, right?
So because the server already deleted it, it's telling us to delete it on our end as well.
So instead of using the destroy function here, we're going to use our private function destroy drink
object instead and pass the object to that function.
Otherwise.
If for some reason.
The object that the server wants us to destroy, we don't have.
We can print something else in the console, like a drink object already destroyed, because either
we already destroyed it or we never had it in the first place.
And of course, in our module script, we'll want to make sure to return our drink table, which contains
our constructor and all that kind of good stuff.
Otherwise, I think this is looking pretty good so far.
I think our logic is quite sound.
Of course we aren't able to test it yet because we still have to, you know, code our local script
that is inside of our 294 GUI.
And we'll also have to script a server script that requires this class and sets it up.
So we still have a little bit more work to do, but we've gotten a decent chunk of it finished.
And before I send you off, actually, one more thing we need to do here is we can go ahead and get
rid of our reference to the player service because in fact, actually we don't need it here on the client
in the first place.
Other than that.
Hang in there.
We're almost finished.
I'll see you in the next lecture.